Skip to content

feat(Popper): add dir prop for RTL/LTR support#2610

Merged
zernonia merged 4 commits into
unovue:v2from
malik-jouda:v2_msh2
Jun 15, 2026
Merged

feat(Popper): add dir prop for RTL/LTR support#2610
zernonia merged 4 commits into
unovue:v2from
malik-jouda:v2_msh2

Conversation

@malik-jouda

@malik-jouda malik-jouda commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked issue

❓ Type of change

  • 📖 Documentation (updates to the documentation, readme or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

  • New Features
    • Added support for specifying reading direction (LTR/RTL) for popper components.
    • Popper positioning, transform origin and arrow alignment now adjust dynamically based on text direction to ensure correct placement in bidirectional and multilingual layouts.

📸 Screenshots (if appropriate)

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Summary by CodeRabbit

  • New Features

    • Popper now supports explicit reading-direction (RTL/LTR) control; content and wrapper reflect the direction.
    • Arrow alignment and transform-origin calculations adapt to the specified direction for correct horizontal positioning.
  • Tests

    • Added RTL/LTR coverage and tests verifying direction propagation and direction-aware positioning behavior.

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds optional dir support to Popper: PopperContent accepts/derives a dir prop and forwards it to the transformOrigin middleware; transformOrigin gained an optional dir option and computes direction-aware transform origins when the arrow is hidden. Tests added for RTL/LTR behavior and wrapper dir attribute.

Changes

Popper direction support

Layer / File(s) Summary
transformOrigin direction-aware logic
packages/core/src/Popper/utils.ts
transformOrigin options gain dir?: Direction; the hidden-arrow alignment mapping is split into noArrowAlignX (direction-aware for top/bottom) and noArrowAlignY (direction-neutral for left/right); coordinate selection updated accordingly.
PopperContent dir prop and propagation
packages/core/src/Popper/PopperContent.vue
Added dir?: Direction to PopperContentProps, resolve reactive dir via useDirection(computed(() => props.dir)), pass dir: dir.value into transformOrigin, and bind/forward :dir="dir" on the wrapper and underlying Primitive.
RTL/LTR tests
packages/core/src/Popper/Popper.test.ts
Import additional Popper components and transformOrigin; tests verify transformOrigin flips horizontal origin for bottom-start/bottom-end under rtl vs ltr, center remains 50%, and wrapper dir attribute reflects PopperContent's dir prop.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I twitch my nose and mark the spot,
LTR or RTL—I trace the dot.
A tiny dir, a subtle flip,
The popper pivots, finds its grip. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature added: RTL/LTR direction support via a new dir prop for Popper components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Apr 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/reka-ui@2610

commit: 8d8fea6

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/core/src/Popper/PopperContent.vue (1)

177-182: New dir prop wiring looks correct.

dir?: Direction is optional and correctly typed against the shared Direction alias. Pairing with useDirection(computed(() => props.dir)) at Line 233 properly falls back to ConfigProvider’s direction and finally 'ltr', preserving existing behavior for unset props.

One small note: the phrasing “when applicable” in the JSDoc is a bit vague — consider clarifying that the direction only affects the computed transform-origin (specifically the start/end alignment on top/bottom placements when the arrow is hidden), and does not automatically flip side. Users in RTL layouts who want side: 'right' to visually appear on the logical start still need to mirror side themselves.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/Popper/PopperContent.vue` around lines 177 - 182, Update
the JSDoc for the dir prop in PopperContent.vue to explicitly state that dir?:
Direction (wired to useDirection(computed(() => props.dir))) only affects
computed transform-origin (i.e., start/end alignment on top/bottom placements
when the arrow is hidden) and does not flip or remap the side prop — users must
mirror side themselves in RTL; make the wording concise and replace "when
applicable" with this specific behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/core/src/Popper/PopperContent.vue`:
- Around line 177-182: Update the JSDoc for the dir prop in PopperContent.vue to
explicitly state that dir?: Direction (wired to useDirection(computed(() =>
props.dir))) only affects computed transform-origin (i.e., start/end alignment
on top/bottom placements when the arrow is hidden) and does not flip or remap
the side prop — users must mirror side themselves in RTL; make the wording
concise and replace "when applicable" with this specific behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6de00e3e-048d-4dbb-96dd-7ebbccc66361

📥 Commits

Reviewing files that changed from the base of the PR and between 78efcf9 and e498973.

📒 Files selected for processing (2)
  • packages/core/src/Popper/PopperContent.vue
  • packages/core/src/Popper/utils.ts

@zernonia zernonia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on RTL support for Popper 🙏 The X/Y split in transformOrigin is the right idea, but I think the feature is incomplete as-is and I'd like to request a change before merging.

The dir prop currently only influences the animation origin, not the actual placement. Floating UI decides RTL alignment-flipping from the computed direction of the floating wrapper element (isRTLgetComputedStyle(floating).direction, where floating is our floatingRef wrapper). But this PR sets :dir only on the inner <Primitive> and feeds the prop solely into transformOrigin.

So the two can disagree — e.g. <PopperContent dir="rtl" /> inside an LTR document positions with LTR alignment (wrapper inherits ltr) but animates from the RTL origin, landing on one side while scaling from the opposite corner.

Could you also bind :dir="dir" on the floatingRef wrapper so Floating UI's isRTL and transformOrigin stay consistent and the prop actually drives placement?

<div
  ref="floatingRef"
  data-reka-popper-content-wrapper=""
  :dir="dir"
  ...
>

Minor follow-ups:

  • Reuse the shared Direction type in utils.ts instead of inlining 'ltr' | 'rtl'.
  • A test covering the RTL flip (and placement/origin consistency) would be great, since the snapshot is the only thing exercising it right now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/core/src/Popper/utils.ts (1)

14-18: ⚡ Quick win

Consider documenting the default direction behavior.

The dir parameter is optional, and when omitted the code defaults to LTR behavior (since options.dir === 'rtl' evaluates to false when dir is undefined). This implicit default is reasonable but not self-documenting.

📝 Suggested improvement

Consider either:

  1. Adding a JSDoc comment documenting that dir defaults to LTR when omitted, or
  2. Making the default explicit:
 export function transformOrigin(options: {
   arrowWidth: number
   arrowHeight: number
   dir?: Direction
 }): Middleware {
+  const dir = options.dir ?? 'ltr'
   return {
     name: 'transformOrigin',
     options,
     fn(data) {
       const { placement, rects, middlewareData } = data

       const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0
       const isArrowHidden = cannotCenterArrow
       const arrowWidth = isArrowHidden ? 0 : options.arrowWidth
       const arrowHeight = isArrowHidden ? 0 : options.arrowHeight

       const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement)
       const noArrowAlignX = {
-        start: options.dir === 'rtl' ? '100%' : '0%',
+        start: dir === 'rtl' ? '100%' : '0%',
         center: '50%',
-        end: options.dir === 'rtl' ? '0%' : '100%',
+        end: dir === 'rtl' ? '0%' : '100%',
       }[placedAlign]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/Popper/utils.ts` around lines 14 - 18, The transformOrigin
middleware accepts an optional dir on the options object but silently treats
undefined as LTR; update transformOrigin to make the default explicit and/or
document it: either (a) add a JSDoc above the transformOrigin function stating
"dir defaults to 'ltr' when omitted" and mention how it's used, or (b) normalize
the value inside transformOrigin by assigning a default (e.g., const dir =
options.dir ?? 'ltr') before any checks (references: transformOrigin and
options.dir) so the LTR default is obvious and maintained.
packages/core/src/Popper/Popper.test.ts (1)

68-110: 💤 Low value

Consider adding a test for default dir behavior.

The current tests verify that explicitly passing dir='rtl' or dir='ltr' correctly sets the wrapper attribute. Consider adding a test case for when dir is omitted to document the expected default behavior.

🧪 Example test for default behavior
it('uses default dir when not explicitly provided', async () => {
  const DefaultPopper = defineComponent({
    setup() {
      return () =>
        h(PopperRoot, null, {
          default: () => [
            h(PopperAnchor, null, { default: () => 'Anchor' }),
            h(PopperContent, {}, { default: () => 'Content' }),
          ],
        })
    },
  })

  const wrapper = mount(DefaultPopper, { attachTo: document.body })
  const wrapperDiv = wrapper.find('[data-reka-popper-content-wrapper]')
  // Document expected default - likely 'ltr' based on useDirection behavior
  expect(wrapperDiv.attributes('dir')).toBeDefined()
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/Popper/Popper.test.ts` around lines 68 - 110, Add a test to
verify the default dir behavior when PopperContent is rendered without a dir
prop: create a component similar to RtlPopper/LtrPopper that renders PopperRoot
with PopperAnchor and PopperContent (no dir prop), mount it, find the element
with data-reka-popper-content-wrapper, and assert the wrapper's dir attribute is
set (or equals the expected default like 'ltr'); reference the existing test
helpers and components PopperRoot, PopperAnchor, PopperContent and the selector
[data-reka-popper-content-wrapper] to locate the element.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/Popper/Popper.test.ts`:
- Around line 28-66: Add unit tests to expand direction-aware coverage for
transformOrigin: reuse baseOptions, makeData and call
transformOrigin(...).fn(...) to assert that top-start/top-end flip X like bottom
(expect RTL start => '100%', LTR start => '0%'); add left/right placement tests
ensuring Y alignment is direction-neutral (e.g., makeData('left-start') and
assert rtl.data.y === ltr.data.y === '0%'); add a test where dir is undefined to
assert default LTR behavior (e.g., transformOrigin({ ...baseOptions
}).fn(makeData('bottom-start')) yields x === '0%'); and add an arrow-visible
case by passing middlewareData.arrow with width/height to makeData and asserting
dir does not change arrow-based origin results.

---

Nitpick comments:
In `@packages/core/src/Popper/Popper.test.ts`:
- Around line 68-110: Add a test to verify the default dir behavior when
PopperContent is rendered without a dir prop: create a component similar to
RtlPopper/LtrPopper that renders PopperRoot with PopperAnchor and PopperContent
(no dir prop), mount it, find the element with data-reka-popper-content-wrapper,
and assert the wrapper's dir attribute is set (or equals the expected default
like 'ltr'); reference the existing test helpers and components PopperRoot,
PopperAnchor, PopperContent and the selector [data-reka-popper-content-wrapper]
to locate the element.

In `@packages/core/src/Popper/utils.ts`:
- Around line 14-18: The transformOrigin middleware accepts an optional dir on
the options object but silently treats undefined as LTR; update transformOrigin
to make the default explicit and/or document it: either (a) add a JSDoc above
the transformOrigin function stating "dir defaults to 'ltr' when omitted" and
mention how it's used, or (b) normalize the value inside transformOrigin by
assigning a default (e.g., const dir = options.dir ?? 'ltr') before any checks
(references: transformOrigin and options.dir) so the LTR default is obvious and
maintained.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ab24d0c-95df-4112-8cab-b228cb30a7ee

📥 Commits

Reviewing files that changed from the base of the PR and between 72d56d8 and 34b3301.

⛔ Files ignored due to path filters (1)
  • packages/core/src/Popper/__snapshots__/Popper.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • packages/core/src/Popper/Popper.test.ts
  • packages/core/src/Popper/PopperContent.vue
  • packages/core/src/Popper/utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/Popper/PopperContent.vue

Comment thread packages/core/src/Popper/Popper.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/core/src/Popper/Popper.test.ts (1)

108-130: ⚡ Quick win

Consider adding edge case: arrow data with centerOffset !== 0.

The current tests cover two scenarios:

  1. No arrow data (arrowData undefined) → isArrowHidden = true
  2. Arrow data with centerOffset: 0isArrowHidden = false

The upstream transformOrigin logic has a third path: arrow data exists but centerOffset !== 0, which sets isArrowHidden = true. While this likely behaves the same as scenario 1, testing it explicitly would verify that the no-arrow alignment (direction-aware noArrowAlignX) is correctly applied when the arrow cannot be centered.

🧪 Example test case
it('centerOffset !== 0 triggers no-arrow alignment (direction-aware)', async () => {
  const withArrow = { arrowWidth: 10, arrowHeight: 5 }
  const cannotCenter = { x: 20, y: 15, centerOffset: 5 } // non-zero offset
  
  // Should use noArrowAlignX, not arrow center
  expect((await run({ ...withArrow, dir: 'rtl' }, 'bottom-start', cannotCenter)).x).toBe('100%')
  expect((await run({ ...withArrow, dir: 'ltr' }, 'bottom-start', cannotCenter)).x).toBe('0%')
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/Popper/Popper.test.ts` around lines 108 - 130, Add a test
that covers the arrow-data-but-not-centerable branch: create cannotCenter = { x:
20, y: 15, centerOffset: 5 } and reuse withArrow = { arrowWidth: 10,
arrowHeight: 5 }, call run({ ...withArrow, dir: 'rtl' }, 'bottom-start',
cannotCenter) and run(..., 'bottom-start', cannotCenter) with dir: 'ltr', and
assert the returned .x uses the no-arrow alignment values ('100%' for rtl, '0%'
for ltr) to ensure the transformOrigin/arrow-centering logic (the run helper and
transformOrigin path that checks centerOffset and isArrowHidden) behaves as
expected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/Popper/Popper.test.ts`:
- Around line 108-130: Add a test that covers the arrow-data-but-not-centerable
branch: create cannotCenter = { x: 20, y: 15, centerOffset: 5 } and reuse
withArrow = { arrowWidth: 10, arrowHeight: 5 }, call run({ ...withArrow, dir:
'rtl' }, 'bottom-start', cannotCenter) and run(..., 'bottom-start',
cannotCenter) with dir: 'ltr', and assert the returned .x uses the no-arrow
alignment values ('100%' for rtl, '0%' for ltr) to ensure the
transformOrigin/arrow-centering logic (the run helper and transformOrigin path
that checks centerOffset and isArrowHidden) behaves as expected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d486f80-ee26-4b73-a209-78d49b4e411a

📥 Commits

Reviewing files that changed from the base of the PR and between 34b3301 and 8d8fea6.

📒 Files selected for processing (1)
  • packages/core/src/Popper/Popper.test.ts

@malik-jouda malik-jouda requested a review from zernonia June 10, 2026 14:45
@malik-jouda

Copy link
Copy Markdown
Contributor Author

@zernonia
Thanks for the review! Requested changes have been applied.

@zernonia zernonia merged commit 01bdb66 into unovue:v2 Jun 15, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants